package net.geant.gembus.metadata;

import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import net.geant.gembus.registry_publisher.RegistryPublishService;

import org.apache.log4j.Logger;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationEvent;
import org.osgi.service.cm.ConfigurationListener;

/**
 * This class is responsible for detecting changes in the gembus.registry.metadata persistent configuration ID (physically stored in etc/gembus.registry.metadata.cfg), and
 * invoking the GEMBus Registry updateMetadata operation.
 * The notion of metadata here refers to any additional information about a service that's not provided by the WSDL/WADL/USDL, such as SLAs, owners, federation levels, 
 * auditing policies, etc.
 * The mechanism devised to "attach" or "pin" this metadata to a service, and being able to update it on the fly while the ESB is running, is to store it in a persistent
 * configuration file "etc/gembus.registry.metadata.cfg". The format of this file is:
 * 
 * {serviceKey}.{metadataAttribute}={value}
 * 
 * For example:
 * 
 * {http\://myservice.org}ServiceA.key1=value1
 * {http\://myservice.org}ServiceA.key2=value2
 * {http\://myservice.org}ServiceA.key3=value3
 * {http\://myservice.org}ServiceA.key4=value4
 * {http\://myservice.org}ServiceB.key1=value1
 * {http\://myservice.org}ServiceB.key2=value2
 * {http\://myservice.org}ServiceB.key3=value3
 * {http\://myservice.org}ServiceB.key4=value4
 * 
 * The above block attaches keys: key1, key2, key3, key4 (with their respective values) to services with id: {http://myservice.org}ServiceA and {http://myservice.org}ServiceB.
 * Note how we escape the colon (:) character in the namespace, otherwise it's picked up as the key-value property separator.
 * 
 * By registering an OSGi ConfigurationListener, the environment will call back any time a config changes, re-parse the properties and update the Registry.
 * 
 * The metadata is encapsulated in a JSON request and sent to the Registry as an HTTP POST.
 * 
 * @author raul
 *
 */
public class MetadataUpdater implements ConfigurationListener {
	private final static Logger LOG = Logger.getLogger(MetadataUpdater.class);
	private final static String SERVICE_METADATA_PERSISTENT_ID = "gembus.registry.metadata";
	
	private BundleContext bundleContext;
	private RegistryPublishService registryService;
	
	// keeps track of the last services for whom metadata was seen, to make it possible to call an HTTP DELETE method if metadata gets detached from the service!
	private Set<String> lastServicesSeen = Collections.synchronizedSet(new HashSet<String>());
	
	/**
	 * Initialises this MetadataUpdater, doing a first cycle of parse + update registry, if the configuration already exists.
	 * @throws Exception
	 */
	public void initialize() throws Exception {
		LOG.info("Initializing GEMBus Registry MetadataUpdater");
		ServiceReference sr = bundleContext.getServiceReference(ConfigurationAdmin.class.getCanonicalName());
		ConfigurationAdmin cfgAdmin = (ConfigurationAdmin) bundleContext.getService(sr);
		Configuration cfg = cfgAdmin.getConfiguration(SERVICE_METADATA_PERSISTENT_ID);
		if (cfg != null) {
			Dictionary<String, String> properties = cfg.getProperties();
			if (properties != null) {
				LOG.info("Discovered properties with persistent id: " + SERVICE_METADATA_PERSISTENT_ID + ": " + properties);
				HashMap<String, HashMap<String, String>> serviceMetadata = parseProperties(properties);
				updateMetadataForServices(serviceMetadata);
			}
		}
	}
	
	/**
	 * This method is called by the OSGi framework when a configuration changes.
	 * It's synchronised to avoid concurrency issues. 
	 */
	public synchronized void configurationEvent(ConfigurationEvent event) {
		LOG.info("Received ConfigurationEvent for PID: " + event.getPid());

		if (!(event.getPid().equals(SERVICE_METADATA_PERSISTENT_ID))) {
			LOG.info("Ignoring ConfigurationEvent for PID: " + event.getPid());
			// ignore the event
			return;
		}

		LOG.info("Processing ConfigurationEvent for PID: " + event.getPid());
		ConfigurationAdmin cfgAdmin = (ConfigurationAdmin) bundleContext.getService(event.getReference());
		try {
			Dictionary<String, String> dict = cfgAdmin.getConfiguration(event.getPid()).getProperties();
			HashMap<String, HashMap<String, String>> allMetadata = parseProperties(dict);
			updateMetadataForServices(allMetadata);
			lastServicesSeen.removeAll(allMetadata.keySet());
			// TODO: with the remaining elements, trigger a Remove Metadata
			// operation on the registry-publisher-rest
			lastServicesSeen.addAll(allMetadata.keySet());
		} catch (Exception e) {
			e.printStackTrace();
		}
		LOG.info("Finished processing registry metadata");
	}
		
	/**
	 * Parses properties from the text format to a HashMap<String, HashMap<String, String>> map
	 * @param properties
	 * @return
	 */
	private HashMap<String, HashMap<String, String>> parseProperties(Dictionary<String, String> properties) {
		HashMap<String, HashMap<String, String>> allServicesMetadata = new HashMap<String, HashMap<String,String>>();
		Enumeration<String> keys = properties.keys();
		LOG.info("Beginning to parse metadata");
		while(keys.hasMoreElements()) {
			String key = keys.nextElement();
			if (key.equals("service.pid") || key.equals("felix.fileinstall.filename")) {
				continue;
			}
			int boundary = key.lastIndexOf('.');
			String serviceId = key.substring(0, boundary);
			String metadataKey = key.substring(boundary + 1);
			HashMap<String, String> myServiceMetadata = allServicesMetadata.get(serviceId);
			if (myServiceMetadata == null) {
				myServiceMetadata = new HashMap<String, String>();
				allServicesMetadata.put(serviceId, myServiceMetadata);
			}
			myServiceMetadata.put(metadataKey, properties.get(key));
		}
		LOG.info("Parsed metadata: " + allServicesMetadata);
		return allServicesMetadata;
	}
	
	/**
	 * Invokes the Registry Service to update the metadata for a series of services.
	 * @param allServicesMetadata
	 * @throws Exception
	 */
	private void updateMetadataForServices(HashMap<String, HashMap<String, String>> allServicesMetadata) throws Exception {
		for (String service : allServicesMetadata.keySet()) {
			LOG.info("Pushing metadata for service " + service + ": " + allServicesMetadata.get(service));
			registryService.updateMetadata(service, allServicesMetadata.get(service));
		}
	}
	
	public BundleContext getBundleContext() {
		return bundleContext;
	}

	public void setBundleContext(BundleContext bundleContext) {
		this.bundleContext = bundleContext;
	}

	public RegistryPublishService getPublishService() {
		return registryService;
	}

	public void setPublishService(RegistryPublishService publishService) {
		this.registryService = publishService;
	}




}